vue原理解析(一)

您所在的位置:网站首页 vue vdom原理 vue原理解析(一)

vue原理解析(一)

#vue原理解析(一)| 来源: 网络整理| 查看: 265

前言

Object.defineProperty是vue2为实现响应式所用到的js中的一个重要的API,我们只有在掌握了这个核心方法之后,才能更深入地理解vue原理。

接下来让我们通过源码解析及手写代码的方式由浅入深地理解Object.defineProperty方法有什么作用,以及vue2中是怎么使用这个方法的,本篇重点主要在于数据代理和数据劫持方面,所以在对vue2源码的解析及代码实现中只会着重讲解这部分。

什么是Object.defineProperty

Object.defineProperty是js提供的一个方法,该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

语法 Object.defineProperty(obj, prop, descriptor) 复制代码 参数

obj(必传)

要定义属性的对象。也就是我们的目标对象,即我们要在哪个对象上进行属性的定义或修改。

prop(必传)

要定义或修改属性的名称。

descriptor(必传)

要定义或修改的属性描述符。可以这么理解,该参数是一个对象,我们可以通过这个对象的一些属性去定义目标属性的特性。

descriptor描述符 数据描述符 value

该属性对应的值,默认为undefined

示例-设置value

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200 }) console.log(divInfo) 复制代码

我们在js中定义一个divInfo对象,然后通过Object.defineProperty方法为该对象添加一个新属性height,接着我们打印divInfo查看结果

image.png

接着我们尝试一下修改新增的属性,并且打印一下divInfo的可枚举属性

示例-属性值能否被修改

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200 }) divInfo.height = 300 console.log(divInfo) console.log(Object.keys(divInfo)) 复制代码

可以看到,虽然我们对height重新赋值为300,但并没有改变divInfo中的height属性值,并且divInfo的枚举属性中没有我们新增的height属性。

image.png

也就是说通过Object.defineProperty方法新增的属性默认不支持修改,且该属性不可枚举。

writable

该属性是否可被修改,默认为false

接着上面的示例,我们设置writable为true

示例-设置writable

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200, writable: true }) divInfo.height = 300 console.log(divInfo) 复制代码

可以看到,设置writable为true后,height属性就支持更改

image.png

enumerable

该属性是否支持枚举,在枚举对象属性时会被枚举到(for...in 或 Object.keys方法),默认为false

示例-设置enumerable

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200, enumerable: true }) console.log(Object.keys(divInfo)) 复制代码

设置enumerable为true后,height属性就支持枚举

image.png

configurable

该属性是否支持删除或重新修改描述符,默认为false

下面查看示例,如果我们不设置configurable,看新增属性能否被删除

示例-属性值能否被删除

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200 }) delete divInfo.height console.log(divInfo) 复制代码

查看打印结果,发现height属性还在,说明通过Object.defineProperty新增的属性默认不支持删除

image.png

接着修改一下示例,设置configurable为true

示例-设置configurable

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200, configurable: true }) delete divInfo.height console.log(divInfo) 复制代码

查看打印结果,height属性已被删除

image.png

下面我们再写个示例验证一下,设置configurable为true后可以重新修改描述符

示例-描述符能否被修改

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200 }) console.log('1:准备开始重写') divInfo.height = 300 console.log('2:查看重写有没生效',divInfo) console.log('3:开始重设writable属性') Object.defineProperty(divInfo,'height',{ value: 300, writable: true }) console.log('4:再次准备开始重写') divInfo.height = 500 console.log('5:查看重写有没生效',divInfo) 复制代码

我们看到当我们想要重设divInfo对象属性的描述符时,控制台会报错,这是因为configurable默认为false,即不允许修改描述符。

image.png

接着,我们在一开始定义属性的地方加上configurable为true

示例-设置configurable验证描述符修改

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ value: 200, configurable: true }) console.log('1:准备开始重写') divInfo.height = 300 console.log('2:查看重写有没生效',divInfo) console.log('3:开始重设writable属性') Object.defineProperty(divInfo,'height',{ value: 300, writable: true }) console.log('4:再次准备开始重写') divInfo.height = 500 console.log('5:查看重写有没生效',divInfo) 复制代码

发现控制台这次没有报错,且设置configurable为true后,我们可以再次修改属性的描述符writable

image.png

存取描述符

注意:当使用了存取描述符,不能再使用数据描述符中的value和writable

get

该属性的getter函数,当访问该属性时,会调用此函数,该函数的返回值会被用作属性的值。 默认为undefined

set

属性的setter函数,当修改该属性时,会调用此函数,该函数接受一个参数(被赋予的新值)

接下来我们写一个示例来感受一下get和set

示例-设置get和set

let divInfo = { width: 300 } Object.defineProperty(divInfo,'height',{ get: function() { console.log('height属性被获取') return 200 }, set: function(value) { console.log(`height属性被修改为${value}`) } }) console.log('1:开始获取height属性') console.log(divInfo.height) console.log('2:开始修改height属性') divInfo.height = 400 复制代码

打印结果如下,当我们访问divInfo.height时触发了get函数,get函数的返回值200作为height属性的值;当我们重新对height属性赋值时,触发了set函数,set函数会接受我们对height属性赋的新值

image.png

但是,在上个示例中,我们的get函数返回的是一个固定值200,那么该如何返回我们设置的值,保证属性每次被修改后,再次获取时获取到的是修改后的最新值?

很简单,我们定义一个初始值就可以

示例-设置get和set

let divInfo = { width: 300 } let originValue = 200 Object.defineProperty(divInfo,'height',{ get: function() { return originValue }, set: function(value) { originValue = value } }) console.log('1:初始获取height属性') console.log(divInfo.height) console.log('2:开始修改height属性') divInfo.height = 400 console.log('3:再次获取height属性') console.log(divInfo.height) 复制代码

image.png

好了,到这里,我们已基本上了解并掌握了Object.defineProperty这个方法,那么这个方法在vue2中是如何使用的呢?

Object.defineProperty在vue2中的使用 数据代理

我们知道,在vue中数据的定义都写在data里面,data可以是一个对象,也可以是一个函数

// data是一个对象 let vm = new Vue({ data: { name: 'John' } }) // data是一个函数 let vm = new Vue({ data() { return { name: 'John' } } }) 复制代码

定义好data后,我们通过this.name(this = vue实例)就可以访问定义在data下的数据,说明data下的这些数据挂载到了vue实例上,那么vue2是如何实现这种数据代理的呢?

答案就在我们的标题中

源码解析

接下来我们就带着疑问在vue2源码(本篇以2.5.2为例)中去查找Object.defineProperty,根据Object.defineProperty去溯源

initState

vue2在初始化initState的时候有一个initData方法会对data进行初始化

function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); } if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } } 复制代码 initData

我们接着查看initData这个方法,可以发现以下几点

在初始化数据的时候,vue2先定义了一个_data的属性用来存储我们定义的data,并且把_data挂载到了vue实例中 vue2在获取我们定义的data的时候,会先进行判断,判断data是否为function,如果为function则执行该function获取数据,否则直接获取,这也就是为什么我们可以定义data为一个对象,也可以定义data为一个函数 vue2在代理数据之前,会进行判断,判断属性名称是否与方法名称或父级传值属性名称重复,如果重复则报错 最终我们找到了proxy这个方法,这个方法就是代理数据的方法,当然这个方法是在循环data的key值中调用的 function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; "development" !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); } // proxy data on instance var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) { var key = keys[i]; { if (methods && hasOwn(methods, key)) { warn( ("Method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasOwn(props, key)) { "development" !== 'production' && warn( "The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.", vm ); } else if (!isReserved(key)) { proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); } 复制代码 proxy

接着查看proxy方法,可以看到传入的target参数就是vue实例,vue2循环data中的属性调用Object.defineProperty方法为每个属性设置了一个getter和setter,并且将属性挂载到了vue实例中,从而实现了数据代理。当我们访问this.xxx的时候,实际上是访问的this._data.xxx,我们对基础类型的数据赋值的时候this.xxx = '123',实际上是对this._data.xxx重新赋值。

var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition); } 复制代码 总结数据代理流程 vue2在初始化的时候,会在initState方法中判断有没有定义data,如果有则执行initData方法 在initData方法中 首先会定义一个_data属性存储data,并将_data挂载到实例中,方便后续操作 其次进行一系列的判断,保证data中的属性合法 利用Object.keys循环遍历data中的key值,调用proxy方法处理数据代理 在proxy方法中,调用Object.defineProperty方法,在vue实例中定义属性(属性名称为data中的key值,属性值通过key从实例中的_data属性取值),从而将data中的属性挂载到vue实例上,实现数据代理 手写代码

了解了Object.defineProperty方法和vue2中的数据代理原理之后,接下来我们尝试着自己手写代码实现数据代理的效果

创建Vue类

我们在新建vue实例的时候,一般会写这样的代码,Vue实际上是一个构造函数,我们会往这个构造函数中传入一系列options(例如el、data、生命周期、methods等),我们称这种方式为options API

let vm = new Vue({ el: '#app', data() { return { } }, ... }) 复制代码

那么我们就可以利用es6的构造函数来创建一个Vue类,新建一个index.js文件,定义一个类,类名为Vue

class Vue { constructor(options) { console.log('获取到传入的数据',options) } } 复制代码 验证Vue类

新建一个index.html文件,引入index.js,创建一个Vue实例

手写数据代理 let vm = new Vue({ data: { divInfo: { width: 300, height: 200 }, testMessage: '测试' } }) 复制代码

访问index.html,查看控制台打印数据,能够正确打印,说明Vue类创建成功

image.png

完善Vue类

index.js

class Vue { constructor(options) { let vm = this; // 先将传入的options存储起来,挂载到实例上 vm.$options = options this.initState(vm) } // 初始化 initState(vm) { let options = vm.$options // 如果有定义data,则初始化数据 if(options.data) { this.initData(vm) } } // 初始化数据 initData(vm) { let data = vm.$options.data // 将data存储到_data中,并将_data挂载到实例上 data = vm._data = typeof(data) === 'function' ? data.call(vm) : data || {} // 获取data的key let keys = Object.keys(data) for(let i = 0; i < keys.length; i++) { this.proxy(vm,'_data',keys[i]) } } // 数据代理 proxy(vm,target,key) { Object.defineProperty(vm,key,{ enumerable: true, configurable: true, get: function proxyGetter() { console.log('【Vue类】获取数据') return vm[target][key] }, set: function proxySetter(value) { console.log('【Vue类】修改数据为',value) vm[target][key] = value } }) } } 复制代码

index.html

let vm = new Vue({ data: { divInfo: { width: 300, height: 200 }, testMessage: '测试' } }) console.log('1:开始获取数据') console.log('获取到的数据为',vm.testMessage) console.log('2:开始修改数据') vm.testMessage = '更改了测试数据' console.log('3:再次获取数据') console.log('获取到的数据为',vm.testMessage) 复制代码

查看打印结果

image.png

在控制台输入vm查看数据挂载情况,可以看到,我们已基本实现数据的代理

image.png

数据劫持

当我们实现了数据代理之后,只是完成了把data中定义的属性挂载到vue实例中去,并且只实现了针对data中基础类型数据的劫持,也就是说,当我们在data中定义了一个对象或数组时,以目前的数据代理方法并不能监听到对象中属性的变化和数组的变化。

所以vue2中还会对data中的复杂类型数据进行数据劫持,针对对象,会循环遍历对象中定义的每个属性,都为其设置一个getter和setter,针对数组,会重写一些数组方法,为后续的响应式做好准备。

源码解析 initData

我们发现在vue2源码的initData方法中,还调用了observe方法

function initData (vm) { var data = vm.$options.data; ... // observe data observe(data, true /* asRootData */); } 复制代码 observe

接着查看observe方法,在该方法中,进行了如下处理:判断数据类型,如果不为object则直接return,否则就return 一个Observer类

/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob } 复制代码 Observer

查看Observer类,这里面对数据进行了判断,如果数据为数组,则重写一些数组方法,并执行observeArray对数组中每项数据继续调用observe方法深入观察;如果数据为对象,则执行walk方法,walk方法会循环对象中每个属性,调用defineReactive方法

var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } }; /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } }; 复制代码 defineReactive

查看defineReactive方法,这是vue处理响应式的核心方法,在这个方法中,会对对象中的每个属性都添加一个getter和setter,并且会递归调用observe处理对象中的属性还是一个对象的情况,修改数据时,如果修改的新值是一个对象,又会继续调用observe

/** * Define a reactive property on an Object. */ function defineReactive ( obj, key, val, customSetter, shallow ) { var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val; /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if ("development" !== 'production' && customSetter) { customSetter(); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = !shallow && observe(newVal); dep.notify(); } }); } 复制代码 重写数组方法

因为Object.defineProperty本身是针对对象进行数据劫持,处理不了数组,而数组的某些方法会更改原数组,如果不对这些方法进行拦截,就无法对数据进行响应式处理。所以为了实现对这些方法的拦截,就需要重写这些数组方法。

var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto);[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // notify change ob.dep.notify(); return result }); }); 复制代码 总结数据劫持流程 在初始化数据initData的方法中,调用observe对传入数据进行观察 在observe方法中,先判断数据类型,如果为基本类型,则不执行后续操作,否则调用Observer方法 Observer方法中 如果数据为数组,则调用observeArray方法循环观察数组中每项数据 如果数据为对象,则直接调用walk方法,通过defineReactive方法为每个属性设置响应属性 defineReactive方法中 传入的数据有可能还是一个数组或对象,所以先对传入的数据调用observe方法做递归处理 通过Object.defineProperty劫持数据,设置getter和setter 在set方法中,因为设置的新值也有可能是复杂类型数据,所以对设置的新值也要执行observe观察 针对数组,重写数组方法 监听能引起原数组改变的方法,做响应式处理,达到数据变化,视图更新 数组新增的元素,有可能是对象,对象又需要进行数据劫持做响应式处理 手写代码

了解了数据劫持原理后,我们就可以尝试着自己来实现数据劫持。

定义observe方法

observe方法中,主要是先将基础类型判断过滤,再执行Observer

function observe(data) { if(typeof(data) !== 'object' || data === null) { return } let ob if(Object.prototype.hasOwnProperty.call(data,'__ob__') && data.__ob__ instanceof Observer) { ob = data.__ob__ }else { ob = new Observer(data) } return ob } 复制代码 定义Observer类

Observer类主要处理对象和数组,给对象和数组中各项数据的对象绑定响应式属性

class Observer { constructor(data) { Object.defineProperty(data, '__ob__', { value: this, enumerable: false, writable: true, configurable: true }); if(Array.isArray(data)) { // 将重写的数组方法放入data的原型链上 data.__proto__ = arrayMethods this.observeArray(data) }else { this.walk(data) } } walk(obj) { let keys = Object.keys(obj) for(let i = 0; i < keys.length; i++) { defineReactive(obj,keys[i],obj[keys[i]]) } } observeArray(array) { for(let i = 0; i < array.length; i++) { observe(array[i]) } } } 复制代码 defineReactive

defineReactive中首先对传入数据进行递归观察,再进行拦截,对set中设置的新值再进行观察

function defineReactive(obj,key,value) { observe(obj[key]) Object.defineProperty(obj,key,{ enumerable: true, configurable: true, get: function reactiveGetter() { console.log(`【reactiveGetter】获取数据${key}`) return value }, set: function reactiveSetter(newValue) { if(newValue === value) { return } observe(newValue) console.log(`【reactiveSetter】修改数据${key}为`,newValue) value = newValue } }) } 复制代码 重写数组方法 // 保存数组原型上的方法 let arrayProto = Array.prototype // 定义一个新的对象,拷贝数组原型方法 let arrayMethods = Object.create(arrayProto) // 要重写的方法 let needEditMethods = [ 'push', // 往数组最后添加一个或多个元素 'pop', // 删除数组中最后一个元素 'shift', // 删除数组中第一个元素 'unshift', // 往数组开头添加一个或多个元素 'splice', // 在数组指定位置删除或添加元素 'sort', // 数组排序 'reverse' // 数组反转 ] needEditMethods.forEach(method => { arrayMethods[method] = function() { // 类数组转为数组 let args = Array.prototype.slice.call(arguments) // 执行原数组方法 let result = arrayProto[method].apply(this,args) let ob = this.__ob__ // 定义一个数组,储存新增加的数据 let insertedArray switch(method) { case 'push': case 'unshift': insertedArray = args break case 'splice': // splice有三个参数,1:起始位置,2:个数,3:要插入的一个或多个值,这里取第3个参数 insertedArray = args.slice(2) break } if(insertedArray) { ob.observeArray(insertedArray) } return result } }) 复制代码 在initData方法中调用observe initData(vm) { let data = vm.$options.data // 将data存储到_data中,并将_data挂载到实例上 data = vm._data = typeof(data) === 'function' ? data.call(vm) : data || {} // 获取data的key let keys = Object.keys(data) for(let i = 0; i < keys.length; i++) { this.proxy(vm,'_data',keys[i]) } observe(data) } 复制代码 验证代码

到这里,我们已基本实现了一个简陋版的vue数据代理和数据劫持,接下来我们验证一下自己写的代码。 在html中定义一些嵌套对象和数组

let vm = new Vue({ data: { divInfo: { style: { width: 200, height: 200 }, class: 'test' }, dataList: [ { id: 1, name: '111' }, { id: 2, name: '222' } ], testMessage: '测试' } }) 复制代码

在浏览器控制台输入vm,查看数据

image.png

继续展开dataList的原型,查看重写的数组方法

image.png

可以看到:

定义的每个数据divInfo、dataList、testMessage都设置好了proxyGetter和proxySetter代理 对象divInfo中每个属性都设置了reactiveGetter和reactiveSetter,实现了数据劫持 数组dataList中每一项中的属性id和name都有reactiveGetter和reactiveSetter 数组dataList原型链中有我们重写的数组方法

接着,测试更改对象值,修改divInfo下style对象中的width属性

分别触发divInfo的代理get,divInfo的响应式get,divInfo下的嵌套对象style的响应式get,最后触发style对象中width属性的响应式set

image.png

测试增加数组元素,往数组dataList新增对象,新增的对象也被设置了响应式get和set

image.png

总结

为了更清晰的了解原理和理清整个流程,我用一张流程图来进行总结。

vue数据劫持原理.jpg



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3